/**
 * Copyright 2006 Brad Reynolds
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.leeloo.databinding.ui.view;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IBreakpointManager;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.jdt.debug.core.IJavaBreakpoint;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.part.ViewPart;
import org.leeloo.core.LeelooPlugin;
import org.leeloo.core.Tag;
import org.leeloo.core.TagService;
import org.leeloo.core.Tags;
import org.leeloo.databinding.internal.ui.BreakpointsBuilder;
import org.leeloo.databinding.internal.ui.ClearViewerAction;
import org.leeloo.databinding.internal.ui.DataBindingUIPlugin;
import org.leeloo.databinding.internal.ui.Databinding;
import org.leeloo.databinding.internal.ui.DebugLifeCycle;
import org.leeloo.databinding.internal.ui.EventViewerBuilder;
import org.leeloo.databinding.internal.ui.IDebugLifeCycleListener;
import org.leeloo.databinding.internal.ui.ITraceListener;
import org.leeloo.databinding.internal.ui.JavaSnippetLoader;
import org.leeloo.databinding.internal.ui.LabeledTreeNode;
import org.leeloo.databinding.internal.ui.LabeledTreeNodeDialog;
import org.leeloo.databinding.internal.ui.LogMessage;
import org.leeloo.databinding.internal.ui.ManageFilterAction;
import org.leeloo.databinding.internal.ui.Resources;
import org.leeloo.databinding.internal.ui.TraceServer;
import org.leeloo.databinding.internal.ui.Viewers;
import org.leeloo.databinding.ui.TagFilterManager;
import org.leeloo.databinding.ui.event.EventParser;
import org.leeloo.databinding.ui.event.FireBindingEventEvent;
import org.leeloo.databinding.ui.event.IEvent;
import org.leeloo.databinding.ui.event.JavaBeanObservableValueConstructedEvent;
import org.leeloo.databinding.ui.event.ResumeEvent;
import org.leeloo.databinding.ui.event.SuspendEvent;
import org.leeloo.databinding.ui.event.ValueChangeEvent;
import org.leeloo.internal.core.ObjectDescriptor;

// TODO look into ILaunchListener and ILaunch to determine process launching

/**
 * Displays a log of databinding events that have occurred within a remote VM.
 * <p>
 * Only works with one process. If multiple processes are ran the behavior is
 * undefined.
 * </p>
 */
public class EventLog extends ViewPart {
    private final static Breakpoint BINDING_BREAKPOINT = new Breakpoint("org.eclipse.jface.databinding.Binding", //$NON-NLS-1$
            61,
            "snippets/Binding.fireBindingEvent.txt"); //$NON-NLS-1$

    private final static Breakpoint BINDING_PROVISIONAL_BREAKPOINT = new Breakpoint("org.eclipse.jface.internal.databinding.provisional.Binding", //$NON-NLS-1$
            61,
            "snippets/Binding.fireBindingEvent.txt"); //$NON-NLS-1$

    private final static Breakpoint VALUE_CHANGE_BREAKPOINT = new Breakpoint("org.eclipse.jface.databinding.observable.value.AbstractObservableValue", //$NON-NLS-1$
            61,
            "snippets/AbstractObservableValue.fireValueChange.txt"); //$NON-NLS-1$

    private final static Breakpoint VALUE_CHANGE_PROVISIONAL_BREAKPOINT = new Breakpoint("org.eclipse.jface.internal.databinding.provisional.observable.value.AbstractObservableValue", //$NON-NLS-1$
            67,
            "snippets/AbstractObservableValue.fireValueChange.txt"); //$NON-NLS-1$

    private final static Breakpoint JAVA_BEAN_OBSERVABLE_BREAKPOINT = new Breakpoint("org.eclipse.jface.internal.databinding.internal.beans.JavaBeanObservableValue", //$NON-NLS-1$
            42,
            "snippets/JavaBeanObservableValue.new.txt"); //$NON-NLS-1$

    /**
     * The viewer of the control.
     */
    private TableViewer viewer;

    /**
     * Table that is the control for the {@link #viewer}.
     */
    private Table table;

    /**
     * Receives events from the remote VM.
     */
    private TraceServer server;

    /**
     * The {@link TableViewer#setInput(Object) input} of the {@link #viewer}.
     */
    private List events;

    /**
     * Receives notifications of events from the {@link #server} and adds them
     * to the {@link #viewer}.
     */
    private ServerTraceListener serverTraceListener;

    /**
     * List of {@link IBreakpoint breakpoints} that have been added and need to
     * be removed when the debug operation is terminated.
     */
    private LinkedList breakpoints;

    /**
     * Listens for notifications of debugger events.
     */
    private DebugLifeCycle debugLifeCycle;

    /**
     * {@link TagService} instance to use for this instance of the view.
     */
    private final TagService tagService = LeelooPlugin.getDefault().getTagService();

    /**
     * Manages tags that have been registered for filtering, selection, and
     * applying filters to the view.
     */
    private TagFilterManager tagFilterManager;

    /**
     * <code>true</code> if the version of databinding is the provisional API
     * version.
     */
    private boolean provisionAPI = false;

    /**
     * Create a new instance.
     */
    public EventLog() {
    }

    /**
     * @return manager that manages the filters that are applied to the view
     */
    public TagFilterManager getTagFilterManager() {
        return tagFilterManager;
    }

    /**
     * Starts the debug process. If already debugging the call is ignored.
     */
    private void startDebugging() {
        if (!server.isRunning()) {
            table.getDisplay().syncExec(new Runnable() {
                public void run() {
                    events.clear();
                    viewer.setInput(events);
                    server.addTraceListener(serverTraceListener = new ServerTraceListener(EventLog.this, tagService));
                }
            });
            tagFilterManager.clear();

            server.start();
            breakpoints = createBreakpoints(server.getLocalPort(), provisionAPI);
            debugLifeCycle.setIgnoreBreakpoints((IBreakpoint[]) breakpoints.toArray(new IBreakpoint[breakpoints.size()]));
        }
    }

    /**
     * Creates the necessary breakpoints using the provided
     * <code>portNumber</code>.
     * 
     * @param portNumber
     * @param provisionalAPI <code>true</code> if using the provisional API
     * @return created breakpoints
     */
    private static LinkedList createBreakpoints(int portNumber, boolean provisionalAPI) {
        LinkedList breakpoints = new LinkedList();

        JavaSnippetLoader snippetLoader = new JavaSnippetLoader(DataBindingUIPlugin.getDefault().getBundle(),
                portNumber);

        Breakpoint breakpoint = BINDING_BREAKPOINT;
        IResource resource = Resources.getResourceForClassName(breakpoint.className);
        if (resource == null) {
            breakpoint = BINDING_PROVISIONAL_BREAKPOINT;
            resource = Resources.getResourceForClassName(breakpoint.className);
            if (resource == null) {
                breakpoint = null;
            }
        }

        provisionalAPI = breakpoint != null && breakpoint == BINDING_PROVISIONAL_BREAKPOINT;

        BreakpointsBuilder builder = new BreakpointsBuilder();
        if (breakpoint != null) {
            builder.addLineBreakpoint(resource,
                    breakpoint.className,
                    breakpoint.lineNumber,
                    snippetLoader.load(breakpoint.snippetPath));
        }

        for (int i = 0; i < 2; i++) {
            breakpoint = (i == 0) ? (provisionalAPI) ? VALUE_CHANGE_PROVISIONAL_BREAKPOINT : VALUE_CHANGE_BREAKPOINT
                    : JAVA_BEAN_OBSERVABLE_BREAKPOINT;
            builder.addLineBreakpoint(Resources.getResourceForClassName(breakpoint.className),
                    breakpoint.className,
                    breakpoint.lineNumber,
                    snippetLoader.load(breakpoint.snippetPath));
        }

        IJavaBreakpoint[] createdBreakpoints = builder.build();
        breakpoints.addAll(Arrays.asList(createdBreakpoints));
        return breakpoints;
    }

    /**
     * Clears breakpoints that were added and shuts down the server.
     */
    private void stopDebugging() {
        if (server.isRunning()) {
            if (breakpoints != null && !breakpoints.isEmpty()) {
                IBreakpointManager manager = DebugPlugin.getDefault().getBreakpointManager();
                try {
                    manager.removeBreakpoints((IBreakpoint[]) breakpoints.toArray(new IBreakpoint[breakpoints.size()]),
                            true);
                } catch (CoreException e) {
                    // ignore, this is cleanup
                }

                breakpoints.clear();
                breakpoints = null;
                debugLifeCycle.setIgnoreBreakpoints(null);
            }

            if (server != null && server.isRunning()) {
                server.removeTraceListener(serverTraceListener);
                server.stop();
            }
        }
    }

    /**
     * Displays the details of the event in a dialog.
     * 
     * @param selection
     */
    private void viewEventDetails(ISelection selection) {
        IEvent event = (IEvent) Viewers.getSingleSelectedObject(selection, FireBindingEventEvent.class);
        String title = null;
        if (event != null) {
            title = Messages.getString("EventLog.binding_event"); //$NON-NLS-1$
        } else {
            event = (IEvent) Viewers.getSingleSelectedObject(selection, ValueChangeEvent.class);
            if (event != null) {
                title = Messages.getString("EventLog.value_change_event"); //$NON-NLS-1$
            }
        }

        if (event != null) {
            LabeledTreeNode node = (LabeledTreeNode) event.getAdapter(LabeledTreeNode.class);
            LabeledTreeNodeDialog dialog = new LabeledTreeNodeDialog(new Shell(table.getShell()),
                    new LabeledTreeNode[] { node },
                    title,
                    tagService);
            dialog.open();
        }
    }

    /**
     * Creates the UI of the view.
     * 
     * @see org.eclipse.ui.part.WorkbenchPart#createPartControl(org.eclipse.swt.widgets.Composite)
     */
    public void createPartControl(Composite parent) {
        EventViewerBuilder builder = new EventViewerBuilder();
        builder.setParent(parent);
        viewer = builder.build();
        tagFilterManager = new TagFilterManager(viewer, tagService);
        table = viewer.getTable();
        events = new ArrayList();
        
        viewer.addDoubleClickListener(new IDoubleClickListener() {
            public void doubleClick(DoubleClickEvent event) {
                viewEventDetails(viewer.getSelection());
            }
        });
        initContextMenu();
        initViewMenu();
        initViewToolbar();

        debugLifeCycle = new DebugLifeCycle();
        debugLifeCycle.addDebugLifeCycleListener(new IDebugLifeCycleListener() {
            /*
             * AsyncExec is wishful thinking in an attempt to allow any events
             * to be returned from the sever before this is processed. Need a
             * way to allow for server events to be published before suspend or
             * resume reliably.
             */
            public void resumed(DebugEvent[] events) {
                if (!table.isDisposed()) {
                    table.getDisplay().asyncExec(new Runnable() {
                        public void run() {
                            addEvent(new ResumeEvent());
                        }
                    });
                }
            }

            public void startDebug(DebugEvent[] events) {
                startDebugging();
            }

            public void stopDebug(DebugEvent[] events) {
                stopDebugging();
            }

            public void suspended(final DebugEvent[] events) {
                if (!table.isDisposed()) {
                    table.getDisplay().asyncExec(new Runnable() {
                        public void run() {
                            addEvent(new SuspendEvent(events));
                        }
                    });
                }
            }
        });

        DebugPlugin.getDefault().addDebugEventListener(debugLifeCycle);
        server = new TraceServer();

        if (DebugUITools.getCurrentProcess() != null) {
            // A process already started, begin debugging.
            startDebugging();
        }
    }

    /**
     * Creates the context menu for the viewer.
     */
    private void initContextMenu() {
        MenuManager manager = new MenuManager();
        Action detailsAction = new Action() {
            public void run() {
                viewEventDetails(viewer.getSelection());
            }
        };
        detailsAction.setText(Messages.getString("EventLog.view_details")); //$NON-NLS-1$
        detailsAction.setToolTipText(Messages.getString("EventLog.view_details_tooltip")); //$NON-NLS-1$
        manager.add(detailsAction);

        Menu menu = manager.createContextMenu(table);
        viewer.getControl().setMenu(menu);

        // Required so other plugins can contribute to menu.
        manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
        // Register the context menu as "the" context menu
        getSite().registerContextMenu(manager, viewer);
    }

    /**
     * Create the view menu.
     */
    // TODO break this out to use extension points
    private void initViewMenu() {
        IMenuManager menuManager = getViewSite().getActionBars().getMenuManager();
        menuManager.add(new ManageFilterAction(viewer, tagFilterManager));
    }
    
    /**
     * Create the view toolbar.
     */
    private void initViewToolbar() {
        IToolBarManager toolbarManager = getViewSite().getActionBars().getToolBarManager();
        toolbarManager.add(new ClearViewerAction(viewer, events));
    }

    /**
     * Clean up.
     * 
     * @see IWorkbenchPart#dispose
     */
    public void dispose() {
        super.dispose();
        stopDebugging();

        if (debugLifeCycle != null) {
            DebugPlugin.getDefault().removeDebugEventListener(debugLifeCycle);
        }

        viewer = null;
        table = null;
    }

    /**
     * Passing the focus request to the viewer's control.
     */
    public void setFocus() {
        viewer.getControl().setFocus();
    }

    /**
     * Receives notifications of messages and populates the UI.
     * 
     * @author Brad Reynolds
     */
    private static class ServerTraceListener implements ITraceListener {
        /**
         * Tag service to add/retrieve tags.
         */
        private TagService tagService;

        /**
         * Parses strings and creates events.
         */
        private EventParser eventParser;

        private EventLog view;

        /**
         * Creates a new instance.
         * 
         * @param view
         * @param tagService
         */
        public ServerTraceListener(EventLog view, TagService tagService) {
            this.view = view;
            this.eventParser = new EventParser();
            this.tagService = tagService;
        }

        /**
         * Adds the event to the list of managed events and populates the UI.
         * 
         * @see org.leeloo.databinding.internal.ui.ITraceListener#message(org.leeloo.databinding.internal.ui.LogMessage)
         */
        public void message(final LogMessage message) {
            final IEvent event = eventParser.parse(message.getMessage());

            if (event instanceof FireBindingEventEvent) {
                FireBindingEventEvent fireBindingEvent = (FireBindingEventEvent) event;
                ObjectDescriptor bindingDescriptor = fireBindingEvent.getBindingDescriptor();

                if (!tagService.contains(bindingDescriptor, Databinding.DATABINDING)) {
                    ObjectDescriptor[] participants = fireBindingEvent.getParticipants();
                    for (int i = 0; i < participants.length; i++) {
                        ObjectDescriptor descriptor = participants[i];
                        tagService.put(descriptor, new Tag(Tags.ID,
                                Integer.toString(descriptor.getIdentityHashCode())));
                        tagService.put(descriptor, new Tag(Databinding.DATABINDING, Boolean.TRUE.toString()));
                    }
                }
            } else if (event instanceof JavaBeanObservableValueConstructedEvent) {
                JavaBeanObservableValueConstructedEvent beanEvent = (JavaBeanObservableValueConstructedEvent) event;
                tagService.put(beanEvent.getObservableDescriptor(), new Tag(Databinding.PROPERTY_NAME,
                        beanEvent.getPropertyName()));

                // these are not displayed
                return;
            }

            view.addEvent(event);
        }
    }

    /**
     * Adds an event to the viewer.
     * 
     * @param event
     */
    private synchronized void addEvent(final IEvent event) {
        if (table.isDisposed()) {
            return;
        }

        events.add(event);

        table.getDisplay().asyncExec(new Runnable() {
            public void run() {
                if (!table.isDisposed()) {
                    viewer.add(event);
                    viewer.reveal(event);
                }
            }
        });
    }

    /**
     * Simple data structure to hold Breakpoint information.
     * 
     * @author Brad Reynolds
     */
    private static class Breakpoint {
        final String className;

        final int lineNumber;

        final String snippetPath;

        Breakpoint(String className, int lineNumber, String snippetPath) {
            this.className = className;
            this.lineNumber = lineNumber;
            this.snippetPath = snippetPath;
        }
    }
}